package org.sample.springbootredis.lock;

import java.util.Objects;
import java.util.concurrent.TimeUnit;

import org.redisson.api.RLock;
import org.redisson.api.RReadWriteLock;
import org.redisson.api.RedissonClient;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

public class RedissonLock {

	private static final Logger LOGGER = LoggerFactory.getLogger(RedissonLock.class);

	private final RedissonClient redisson;

	private final LockType type;

	private final String key;

	private final long leaseMillis;

	private final long waitMillis;

	private RLock acquiredLock;

	public static RedissonLock create(RedissonLockConfig config) {
		return new RedissonLock(config);
	}

	private RedissonLock(RedissonLockConfig config) {
		Objects.requireNonNull(config);

		this.redisson = config.redisson;
		this.type = config.type;
		this.key = config.key;
		this.leaseMillis = config.leaseMillis;
		this.waitMillis = config.waitMillis;
	}

	public boolean isLocked() {
		return this.acquiredLock != null;
	}

	private boolean tryLock(RLock lock, long waitMills, long leaseMillis) {
		boolean isLocked = false;
		try {
			LOGGER.info(String.format("Thread is acquiring %s (%s).", this.type.getName(), this.key));
			isLocked = lock.tryLock(waitMills, leaseMillis, TimeUnit.MILLISECONDS);
		} catch (InterruptedException e) {
			LOGGER.warn(String.format("Thread is interrupted when wait for %s (%s).", this.type.getName(), this.key));
		}
		if (isLocked) {
			this.acquiredLock = lock;
			LOGGER.info(String.format("Thread acquired %s (%s).", this.type.getName(), this.key));
		} else {
			LOGGER.info(String.format("Thread cannot acquire %s (%s).", this.type.getName(), this.key));
		}
		return isLocked;
	}

	public boolean tryLock() {
		return tryLock(this.waitMillis, this.leaseMillis);
	}

	public boolean tryLock(long waitMills, long leaseMillis) {
		if (isLocked()) {
			throw new UnsupportedOperationException("It has been locked and cannot be locked again.");
		}

		RLock lock;
		RReadWriteLock rwLock;
		switch (this.type) {
			case LOCK:
				lock = redisson.getLock(key);
				break;

			case READ_LOCK:
				rwLock = redisson.getReadWriteLock(key);
				lock = rwLock.readLock();
				break;

			case WRITE_LOCK:
				rwLock = redisson.getReadWriteLock(key);
				lock = rwLock.writeLock();
				break;

			default:
				LOGGER.warn(String.format("Unknown lock type %s", type.getName()));
				lock = redisson.getLock(key);
		}

		return tryLock(lock, waitMills, leaseMillis);
	}

	public void unlock() {
		if (!isLocked()) {
			throw new UnsupportedOperationException("It has been unlocked or not been locked yet.");
		}

		this.acquiredLock.unlock();
		this.acquiredLock = null;
		LOGGER.info(String.format("Thread released %s (%s).", this.type.getName(), key));
	}

}
